iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 29
2
Modern Web

從比入門再往前一點開始,一直到深入React.js系列 第 29

【Day.29】React進階 - 以Redux Thunk處理非同步資料流

  • 分享至 

  • xImage
  •  

(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問

很多時候,我們的state必須要透過HTTP Request從後端取得。然而發送Request常用的fetch或是axios是非同步的。雖然我們可以透過以下方式把資料送進去Redux:

fetch( "URL", {
        method: "GET"
    })
    .then(res => res.json())
    .then(data => {
        dispatch({type:"TYPE", payload: {data}});
    })
    .catch(e => {
        /*發生錯誤時要做的事情*/
    }
)

但最理想的狀況還是讓這個fetch的過程被模組、抽象化,也就是不應該還要讓UI繪製程式還要自己去call fectch API。我們希望UI繪製程式只需要呼叫一個函式,從fetch到更新Redux的這串過程都會完成

不論是在Flux,還是傳統的MVC、MVP、MVVM觀念下,都希望把資料處理的程式抽離UI繪製的程式,而不是讓兩者混雜在一起

講白一點,我們的流程本來是:

  1. 操作者呼叫dispatch
  2. Redux判斷action
  3. Redux根據action對state做出對應修改

現在我們希望流程改成這樣:

  1. 操作者呼叫dispatch
  2. 一個遇到非同步事件,就會等到非同步事件結束才再次呼叫dispatch、傳遞action的模組程式
  3. Redux判斷action
  4. Redux根據action對state做出對應修改

一般會把2這種在本來行為之間(1和3)的加工過程稱為middleware(中介層)。

Redux-Thunk

Redux-Thunk就是一個簡化Redux處理非同步事件的中介層套件。它的運作流程是這樣的,基本上就跟我們剛剛說的差不多:


上圖來源

Redux middleware與Redux-Thunk的使用

接下來我們會實際操作一次Redux-Thunk,試著把MenuItem的資料改成從後端取得。資料會用我放在自己github的台灣的縣市列表JSON檔

{
    "cityList":[
        "臺北市",
        "基隆市",
        "新北市",
        (略......)
    ]
}

1. 安裝

請打開terminal,輸入:

npm install redux-thunk --save

2. 建立src/model/action.js

一般會在這裡以變數統一管理action字串。不過這裡我們先拿來放等等要定義的fetch

在src/model/action.js中,定義一個函式,把item改成fetch函式得到的資料。Redux-Thunk會把dispatch函式當成函式的參數傳入。我們則要在非同步事件結束後再次呼叫dispatch,給予對應的action和payload

因為現在我們的reducer還沒有這種一次修改所有資料的action,我們先加一個SET_ITEM,等等再加回reducer中。

  • src/model/action.js
export const fetchCityItem = () => {
    return (dispatch) => {
        fetch( "https://raw.githubusercontent.com/JiaAnTW/mask/master/dist.json", {
            method: "GET"
        })
        .then(res => res.json())
        .then(data => {
            dispatch({
                type: "SET_ITEM",
                payload: {itemNewArr: data["cityList"]}
            });
        })
        .catch(e => {
            console.log(e);
        })
    };
};

3. 加入Redux-Thunk到Redux中

Redux提供了applyMiddleware這個函式來讓我們安裝middleware到Redux中。用法是將applyMiddleware(中介層1,中介層2,...)放在createStore的第二個參數中。

現在,請引入Redux-Thunk的thunk和Redux的applyMiddleware,並加入我們的store中:

  • src/model/store.js
import {createStore, applyMiddleware } from "redux";
import {itemReducer} from "./reducer.js";
import thunk from "redux-thunk";

const itemStore = createStore(itemReducer,applyMiddleware(thunk)); 

export {itemStore};

這樣使用Redux-thunk的架構就完成了。

4. 補回reducer處理 SET_ITEM 的case

  • src/model/reducer.js
const initState = {
    menuItemData: [
        "Like的發問",
        "Like的回答",
        "Like的文章",
        "Like的留言"
    ],
  };
  
const itemReducer = (state = initState, action) => {
    switch (action.type) {
      case 'ADD_ITEM': {
        const menuItemCopy = state.menuItemData.slice();
        return { menuItemData: [action.payload.itemNew].concat(menuItemCopy) };
      }
      case 'SET_ITEM': {
        return { menuItemData: action.payload.itemNewArr };
      }
      case 'CLEAN_ITEM': {
        return { menuItemData: [] };
      }
      default:
        return state;
    }
};

export {itemReducer};

5. 在需要的地方,以dispatch呼叫fetchCityItem

觸發Redux-Thunk的方式,是在需要的地方呼叫

dispatch( 剛剛定義的非同步函式() );

也就是你可以在src/page/MenuPage新增一個按鈕:

<button onClick={()=>{
    dispatch(
        fetchCityItem()
    ); 
}}>抓取並修改menuItem</button>

按下去之後,Redux就會根據我們剛剛定義的內容,先執行發送Http Request,等資料回來,才執行dispatch,把action和剛剛放入payload的縣市資料丟到reducer去更新。

  • src/page/MenuPage.js
import React, { useState, useReducer,useMemo,useEffect} from 'react';
import useMouseY from '../util/useMouseY';

import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext } from '../context/ControlContext';

import { useSelector, useDispatch } from 'react-redux';
import { fetchCityItem } from '../model/action';

const reducer = function(state, action){
    switch(action.type){
        case "SWITCH":
            return !state;
        default:
            throw new Error("Unknown action");
    }
}

const MenuPage = () =>{
    const [isOpen, isOpenDispatch] = useReducer(reducer,true);

    const menuItemData = useSelector(state => state.menuItemData);
    const dispatch = useDispatch();

    let menuItemArr = useMemo(()=>menuItemData.map((wording) => <MenuItem text={wording} key={wording}/>),[menuItemData]);

    return (
        <OpenContext.Provider value={{ 
            openContext: isOpen, 
            setOpenContext: isOpenDispatch
        }} >
            <Menu title={"Andy Chang的like"}>
                {menuItemArr}
            </Menu>
            <button onClick={()=>{
                dispatch({
                    type: "ADD_ITEM",
                    payload: {itemNew:"測試資料"}
                }); 
            }}>更改第一個menuItem</button>
            <button onClick={()=>{
                dispatch(
                    fetchCityItem()
                ); 
            }}>抓取並修改menuItem</button>
        </OpenContext.Provider>
    );
}

export default MenuPage;

參考資料

Thunks in Redux: The Basics


上一篇
【Day.28】React進階 - 導入Redux,讓元件溝通更簡潔
下一篇
【Day.30】React進階 - Styled-Components: React的CSS解決方案 | 系列總結
系列文
從比入門再往前一點開始,一直到深入React.js30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
AndrewYEE
iT邦新手 3 級 ‧ 2023-02-07 15:50:57

這裡的範例跟書中的不太一樣,我個人覺得這裡的範例對我比較重要一些XD

0
AndrewYEE
iT邦新手 3 級 ‧ 2023-02-07 17:31:05

我的理解:

let add1 = x => y => z => x + y + z   //add1 = (x)=>{ return (x)=>{ (y) => { (z) => { x + y + z } } } }
let add2 = add1(1)                    //add2 = (y)=>{ return (y)=>{ (z) => { 1 + y + z } } }
let add3 = add2(2)                    //add3 = (z)=>{ return 1 + 2 + z }
add3(3)                               //add3 = (3)=>{ return 1 + 2 + 3 }

function add1(x){
  //此時add2能看到x,所以不用具體從參數帶入
  function add2(y){
    //此時add3能看到x,y,所以不用具體從參數帶入
    return add3(z){
      x + y + z
    }
  }
}

//=============================//
const logMiddleWare = store => next => action => {
			console.log("dispatching", action);
			next(action);
}

function logMiddleWare(store){
			//此時Func1能看到store,所以不用具體從參數帶入
			function Func1(next){
						//此時Func2能看到store,next,所以不用具體從參數帶入
						function Func2(action){
									next(action);
						}
			}
}
//在Redux thunk中,Next其實可以當作dispatch
//在Redux thunk中,store其實可以當作State

//=======Redux thunk源碼========//
function createThunkMiddleware(extraArgument) {
    // 這是 middleware 基本的寫法
    return ({ dispatch, getState }) =>
        (next) =>
            (action) => {
                // action 就是透過 action creators 傳進來的東西,在 redux-thunk 中會是 async function
                if (typeof action === 'function') {
                    // 在這裡回傳「執行後的 async function」
                    return action(dispatch, getState, extraArgument);
                }

                // 如果傳進來的 action 不是 function,則當成一般的 action 處理
                return next(action);
            };
}

//=======在Redux thunk範例========//
// fetchTodoById is the "Thunk Action Creator"
export function fetchTodoById(todoId) {
	  // fetchTodoByIdThunk is the "Thunk Function"
	  return async function fetchTodoByIdThunk(dispatch, getState) {
		    const response = await client.get(`/fakeApi/todo/${todoId}`)
		    dispatch(todosLoaded(response.todos))
	  }
}

//use
function TodoComponent({ todoId }) {
  const dispatch = useDispatch()
  const onFetchClicked = () => {
    // Calls the thunk action creator, and passes the thunk function to dispatch
    dispatch(fetchTodoById(todoId))
  }
}

//=======將範例套用到源碼中========//
function createThunkMiddleware(todoId) {
    // 這是 middleware 基本的寫法
    return ({ dispatch, getState }) =>
        (dispatch) =>
            (fetchTodoById) => {
                // action 就是透過 action creators 傳進來的東西,在 redux-thunk 中會是 async function
                if (typeof fetchTodoById=== 'function') {
                    // 在這裡回傳「執行後的 async function」
                    return fetchTodoById(dispatch, getState, todoId);
                }

                // 如果傳進來的 action 不是 function,則當成一般的 action 處理
                return dispatch(fetchTodoById);
            };
}

我要留言

立即登入留言